const express = require('express')
const app = express()
const port = 9531 // 3030 // 9531
const path = require('path')
const fs = require('fs');
const fse = require('fs-extra')
const multiparty = require('multiparty')

app.use((req, res, next) => {
    // 请求头允许跨域
    res.setHeader('Access-Control-Allow-Origin', '*')
    res.setHeader('Access-Control-Allow-Headers', '*')
    next()
})

app.options('*', (req, res) => {
    res.sendStatus(200)
})

app.listen(port, () => console.log(`vue3完整版大文件上传：监听${port}端口`))
const currentDate = new Date();
const dirDate = currentDate.getFullYear() + '-' + currentDate.getMonth() + '-' + currentDate.getDate()

// 大文件存储目录
const UPLOAD_DIR = path.resolve(__dirname, 'uploadResource' + '/' + dirDate)
// 创建临时文件夹用于临时存储 所有的文件切片
const getChunkDir = (fileHash) => {
    // 添加 chunkCache 前缀与文件名做区分
    // target/chunkCache_fileHash值
    return path.resolve(UPLOAD_DIR, `chunkCache_${fileHash}`)
}

// 处理切片上传
app.post('/prod-api/upload', async (req, res) => {
    try {
        // 处理文件表单
        const form = new multiparty.Form()
        form.parse(req, async (err, fields, files) => {
            console.log('-------------upload', files)
            if (err) {
                res.send({ code: -1, msg: '单片上传失败', data: err })
                return false
            }
            // fields是body参数
            // 文件hash ，切片hash ，文件名
            const { fileHash, chunkHash, fileName } = fields
            // files是传过来的文件所在的真实路径以及内容
            const { chunkFile } = files

            // 创建一个临时文件目录用于 临时存储所有文件切片
            const chunkCache = getChunkDir(fileHash)

            // 检查 chunkDir临时文件目录 是否存在，如果不存在则创建它。
            if (!fse.existsSync(chunkCache)) {
                await fse.mkdirs(chunkCache)
            }

            //   将上传的文件切片移动到指定的存储文件目录
            //  fse.move 方法默认不会覆盖已经存在的文件。
            //   将 overwrite: true 设置为 true，这样当目标文件已经存在时，将会被覆盖。
            //   把上传的文件移动到 /target/chunkCache_ + chunkHash
            await fse.move(chunkFile[0].path, `${chunkCache}/${chunkHash}`, {
                overwrite: true,
            })
            res.send({
                code: 0,
                msg: '单片上传完成',
                data: { fileHash, chunkHash, fileName },
            })
        })
    } catch (errB) {
        res.send({ code: -1, msg: '单片上传失败', data: errB })
    }
})

// 处理请求参数
const resolvePost = (req) => {
    // 所有接收到的数据块拼接成一个字符串，然后解析为 JSON 对象。
    return new Promise((resolve) => {
        let body = [] // 使用数组而不是字符串来避免大字符串的内存问题
        // 监听请求对象 req 的 data 事件。每当有数据块传输过来时，处理程序就会被调用。
        req.on('data', (data) => {
            // 假设数据是 Buffer，将其追加到数组中
            body.push(data)
        })
        // 监听请求对象 req 的 end 事件。当所有数据块接收完毕时
        req.on('end', () => {
            // 使用 Buffer.concat 将所有数据块合并为一个 Buffer
            const buffer = Buffer.concat(body)
            // 将 Buffer 转换为字符串（假设是 UTF-8 编码）
            const stringData = buffer.toString('utf8')
            try {
                // 尝试解析 JSON 字符串
                const parsedData = JSON.parse(stringData)
                // 如果解析成功，则 resolve
                resolve(parsedData)
            } catch (error) {
                // 如果解析失败，则 reject
                reject(new Error('参数解析失败'))
            }
            // 可以添加一个 'error' 事件监听器来处理任何可能出现的错误
            req.on('error', (error) => {
                reject(error)
            })
        })
    })
}

// 把文件切片写成总的一个文件流
const pipeStream = (path, writeStream) => {
    return new Promise((resolve) => {
        // 创建可读流
        const readStream = fse.createReadStream(path).on('error', (err) => {
            // 如果在读取过程中发生错误，拒绝 Promise
            reject(err)
        })
        // 在一个指定位置写入文件流
        readStream.pipe(writeStream).on('finish', () => {
            // 写入完成后，删除原切片文件
            fse.unlinkSync(path)
            resolve()
        })
    })
}
// 合并切片
const mergeFileChunk = async (chunkSize, fileHash, filePath) => {
    try {
        // target/chunkCache_fileHash值
        const chunkCache = getChunkDir(fileHash)
        // 读取 临时所有切片目录 chunkCache 下的所有文件和子目录，并返回这些文件和子目录的名称。
        const chunkPaths = await fse.readdir(chunkCache)

        // 根据切片下标进行排序
        // 否则直接读取目录的获得的顺序会错乱
        chunkPaths.sort((a, b) => a.split('-')[1] - b.split('-')[1])

        let promiseList = []
        for (let index = 0; index < chunkPaths.length; index++) {
            // target/chunkCache_hash值/文件切片位置
            let chunkPath = path.resolve(chunkCache, chunkPaths[index])
            // 根据 index * chunkSize 在指定位置创建可写流
            let writeStream = fse.createWriteStream(filePath, {
                start: index * chunkSize,
            })
            promiseList.push(pipeStream(chunkPath, writeStream))
        }

        // 使用 Promise.all 等待所有 Promise 完成
        // (相当于等待所有的切片已写入完成且删除了所有的切片文件)
        Promise.all(promiseList)
            .then(() => {
                console.log('所有文件切片已成功处理并删除')
                // 在这里执行所有切片处理完成后的操作
                // 递归删除缓存切片目录及其内容 (注意，如果删除不存在的内容会报错)
                if (fse.pathExistsSync(chunkCache)) {
                    fse.remove(chunkCache)
                    console.log(`chunkCache缓存目录删除成功`)
                    // 合并成功，返回 Promise.resolve
                    return Promise.resolve()
                } else {
                    console.log(`${chunkCache} 不存在，不能删除`)

                    return Promise.reject(`${chunkCache} 不存在，不能删除`)
                }
            })
            .catch((err) => {
                console.error('文件处理过程中发生错误：', err)
                // 在这里处理错误，可能需要清理资源等
                return Promise.reject(`'文件处理过程中发生错误：${err}`)
            })
    } catch (err) {
        console.log(err, '合并切片函数失败')
        return Promise.reject(`'合并切片函数失败：${err}`)
    }
}
// 提取文件后缀名
const extractExt = (fileName) => {
    // 查找'.'在fileName中最后出现的位置
    const lastIndex = fileName.lastIndexOf('.')
    // 如果'.'不存在，则返回空字符串
    if (lastIndex === -1) {
        return ''
    }
    // 否则，返回从'.'后一个字符到fileName末尾的子串作为文件后缀（包含'.'）
    return fileName.slice(lastIndex)
}

app.post('/prod-api/merge', async (req, res) => {
    try {
        // 在上传完所有切片后就要调合并切片
        const data = await resolvePost(req)
        // 切片大小 文件名 文件hash
        const { chunkSize, fileName, fileHash } = data
        // 提取文件后缀名
        const ext = extractExt(fileName)
        // 整个文件路径 /target/文件hash.文件后缀
        const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`)
        // 开始合并切片
        await mergeFileChunk(chunkSize, fileHash, filePath)
        res.send({
            code: 0,
            msg: '文件合并成功',
        })
    } catch (e) {
        res.send({
            code: -1,
            data: e,
            msg: '文件合并失败！',
        })
    }
})
// 返回已上传的所有切片名
const createUploadedList = async (fileHash) => {
    // 如果存在这个目录则返回这个目录下的所有切片
    // fse.readdir返回一个数组，其中包含指定目录中的文件名。
    return fse.existsSync(getChunkDir(fileHash))
        ? await fse.readdir(getChunkDir(fileHash))
        : []
}
// 验证是否存在已上传切片
app.post('/prod-api/verify', async (req, res) => {
    try {
        const data = await resolvePost(req)
        const { fileHash, fileName } = data

        // 文件名后缀
        const ext = extractExt(fileName)
        // 最终文件路径
        const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`)

        // 如果已经存在文件则标识文件已存在，不需要再上传
        if (fse.existsSync(filePath)) {
            res.send({
                code: 0,
                data: {
                    shouldUpload: false,
                    uploadedList: [],
                },
                msg: '已存在该文件',
            })
        } else {
            // 否则则返回文件已经存在切片给前端
            // 告诉前端这些切片不需要再上传
            res.send({
                code: 0,
                data: {
                    shouldUpload: true,
                    uploadedList: await createUploadedList(fileHash),
                },
                msg: '需要上传文件/部分切片',
            })
        }
    } catch (err) {
        res.send({ code: -1, msg: '上传失败', data: err })
    }
})
app.post('/prod-api/getFiles', async (req, res) => {
    // 读取目录下的所有文件
    const queryParams = await resolvePost(req);
    const { pageNumber, pageSize, fileName, startTime, endTime } = queryParams;
    console.log('----------queryParams', queryParams)
    fs.readdir(UPLOAD_DIR, function (err, files) {
        let fileInfoList = []
        if (err) {
            res.json({ code: 200, msg: '查询成功', data: fileInfoList });
            return console.log('Unable to scan directory: ' + err);
        }
        if (files.length === 0) {
            res.json({ code: 200, msg: '查询成功', data: fileInfoList });
        } else {
            const startIndex = (pageNumber - 1) * pageSize;
            const endIndex = pageNumber * pageSize;
            files.forEach(function (file) {
                // 获取文件完整路径
                const filePath = path.join(UPLOAD_DIR, file);
                // 使用 fs.stat() 方法获取文件信息
                fs.stat(filePath, (err, stats) => {
                    if (err) throw err;
                    // 构建文件信息对象
                    const fileInfo = {
                        fileName: file,
                        fileSize: stats.size,
                        createTime: stats.mtime.toLocaleString(), // 输出 ISO 8601 格式的 UTC 时间  toLocaleString输出本地时间
                        isFile: stats.isFile(),
                        url: UPLOAD_DIR + '\\' + file,
                        isDirectory: stats.isDirectory()
                    };
                    fileInfoList.push(fileInfo);
                    if (fileInfoList.length === files.length) {
                        fileInfoList = fileInfoList.sort((a,b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
                        if (startTime) fileInfoList = fileInfoList.filter(item => new Date(item.createTime).getTime() >= new Date(startTime).getTime());
                        if (endTime) fileInfoList = fileInfoList.filter(item => new Date(item.createTime).getTime() <= new Date(endTime).getTime());
                        if (fileName) fileInfoList = fileInfoList.filter(item => fileName && item.fileName.indexOf(fileName) !== -1);
                        fileInfoList = fileInfoList.filter((item, index) =>
                            startIndex <=index && index < endIndex
                        );
                        res.json({ code: 200, msg: '查询成功', data: fileInfoList });
                    }
                });
            })
        }
    });
});
app.post('/prod-api/download', async (req, res) => {
    const {filename} = await resolvePost(req);

    console.log('-----------filename-------------',filename)

    const filePath = path.join(UPLOAD_DIR, filename);
    console.log('-----------filePath-------------',filePath)
    // 检查文件是否存在
    fs.access(filePath, fs.constants.F_OK, (err) => {
        if (err) {
            res.status(404).send('File not found');
            return;
        }

        // 获取文件统计信息
        fs.stat(filePath, (err, stats) => {
            if (err) {
                res.status(500).send('Internal Server Error');
                return;
            }

            const fileSize = stats.size;
            const range = req.headers.range;

            if (!range) {
                // 如果没有 Range 头，返回整个文件
                res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
                res.setHeader('Content-Length', fileSize);
                res.sendFile(filePath);
                return;
            }

            // 解析 Range 头
            const positions = range.replace(/bytes=/, "").split("-");
            const start = parseInt(positions[0], 10);
            const end = (positions[1] && positions[1] < fileSize ? parseInt(positions[1], 10) : fileSize) - 1;

            console.log('---Range----', start, end, fileSize)
            if (start >= fileSize || end > fileSize) {
                res.status(416).send('Requested Range Not Satisfiable');
                return;
            }

            const chunkSize = (end - start) + 1;
            const fileStream = fs.createReadStream(filePath, { start, end });

            const headers = {
                'Content-Range': `bytes ${start}-${end}/${fileSize}`,
                'Accept-Ranges': 'bytes',
                'Content-Length': chunkSize,
                'Content-Type': 'application/octet-stream', // 根据文件类型调整
                'Content-Disposition': `attachment; filename="${filename}"`
            };

            res.writeHead(206, headers);
            fileStream.pipe(res);

            fileStream.on('error', () => {
                res.status(500).send('Internal Server Error');
            });

            fileStream.on('close', () => {
                res.end();
            });
        });
    });
});
